use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::path::{Path, PathBuf};

use log::{error, info, warn};
use runtime_tracing::{CallKey, Line, StepId, TypeKind, TypeRecord, TypeSpecificInfo, NO_KEY};

use crate::{
    db::{Db, DbRecordEvent},
    expr_loader::ExprLoader,
    lang::{lang_from_context, Lang},
    replay::Replay,
    task::{
        Action, BranchesTaken, CoreTrace, FlowEvent, FlowMode, FlowStep, FlowUpdate, FlowUpdateState,
        FlowUpdateStateKind, FlowViewUpdate, Iteration, Location, Loop, LoopId, LoopIterationSteps, Position, RRTicks,
        StepCount, TraceKind,
    },
    value::{to_ct_value, Value, ValueRecordWithType},
};

const STEP_COUNT_LIMIT: usize = 10000;
const RETURN_VALUE_RR_DEPTH_LIMIT: usize = 7;
const LOAD_FLOW_VALUE_RR_DEPTH_LIMIT: usize = 2;

#[derive(Debug)]
pub struct FlowPreloader {
    pub expr_loader: ExprLoader,
}

#[allow(clippy::new_without_default)]
impl FlowPreloader {
    pub fn new() -> Self {
        FlowPreloader {
            expr_loader: ExprLoader::new(CoreTrace::default()),
        }
    }

    pub fn load(&mut self, location: Location, mode: FlowMode, kind: TraceKind, replay: &mut dyn Replay) -> FlowUpdate {
        info!("flow: load: {:?}", location);
        let path_buf = PathBuf::from(&location.path);
        match self.expr_loader.load_file(&path_buf) {
            Ok(_) => {
                info!("  expression loader complete!");
                let mut call_flow_preloader: CallFlowPreloader =
                    CallFlowPreloader::new(self, location.clone(), HashSet::new(), HashSet::new(), mode, kind);
                call_flow_preloader.load_flow(location, replay)
            }
            Err(e) => {
                warn!("can't process file {}: error {}", location.path, e);
                FlowUpdate::error(&format!("can't process file {}", location.path))
            }
        }
    }

    pub fn load_diff_flow(
        &mut self,
        diff_lines: HashSet<(PathBuf, i64)>,
        db: &Db,
        trace_kind: TraceKind,
        replay: &mut dyn Replay,
    ) -> Result<FlowUpdate, Box<dyn Error>> {
        info!("load_diff_flow");
        for diff_line in &diff_lines {
            match self.expr_loader.load_file(&diff_line.0) {
                Ok(_) => {
                    continue;
                }
                Err(e) => {
                    warn!("can't process file {}: error {}", diff_line.0.display(), e);
                    return Err(format!("can't process file {}", diff_line.0.display()).into());
                    // FlowUpdate::error(&format!("can't process file {}", diff_line.0.display()));
                }
            }
        }

        let mut diff_call_keys = HashSet::new();
        // put breakpoints on all of them
        for diff_line in &diff_lines {
            let _ = replay.add_breakpoint(&diff_line.0.display().to_string(), diff_line.1)?;
        }
        // TODO: breakpoints on function entries or function names as well
        //   so => we can count how many stops?
        //
        // just continue for now in next diff flow step; and if we go through the function/function entry line or
        // breakpoint; count a next call for it;
        // maybe this will just work because they're registered as loop first line

        for step in db.step_from(runtime_tracing::StepId(0), true) {
            if diff_lines.contains(&(PathBuf::from(db.paths[step.path_id].clone()), step.line.0)) {
                diff_call_keys.insert(step.call_key.0);
                let location = db.load_location(step.step_id, step.call_key, &mut self.expr_loader);
                // register an artifficial loop for each function from the diff,
                //   that we track, so we can visualize different global calls to those functions
                //   with sliders/count etc:
                self.expr_loader.register_loop(
                    Position(location.function_first),
                    Position(location.function_last),
                    &PathBuf::from(&db.paths[step.path_id]),
                );
            }
        }

        let mut call_flow_preloader = CallFlowPreloader::new(
            self,
            Location::default(),
            diff_lines,
            diff_call_keys,
            FlowMode::Diff,
            trace_kind,
        );
        let location = Location {
            line: 1,
            ..Location::default()
        };
        Ok(call_flow_preloader.load_flow(location, replay))
    }

    // fn load_file(&mut self, path: &str) {
    // self.expr_loader.load_file(&PathBuf::from(path.to_string())).unwrap();
    // }

    pub fn get_var_list(&self, line: Position, location: &Location) -> Option<Vec<String>> {
        self.expr_loader.get_expr_list(line, location)
    }

    // fn get_function_location(&self, location: &Location, line: &Line) -> Location {
    // self.expr_loader.find_function_location(location, line)
    // }
}

pub struct CallFlowPreloader<'a> {
    flow_preloader: &'a FlowPreloader,
    location: Location,
    active_loops: Vec<Position>,
    last_step_id: StepId,
    last_expr_order: Vec<String>,
    diff_lines: HashSet<(PathBuf, i64)>,
    diff_call_keys: HashSet<i64>, //  TODO: if we add Eq, Hash it seems we can do CallKey
    mode: FlowMode,
    trace_kind: TraceKind,
    lang: Lang,
}

impl<'a> CallFlowPreloader<'a> {
    pub fn new(
        flow_preloader: &'a FlowPreloader,
        location: Location,
        diff_lines: HashSet<(PathBuf, i64)>,
        diff_call_keys: HashSet<i64>,
        mode: FlowMode,
        trace_kind: TraceKind,
    ) -> Self {
        CallFlowPreloader {
            flow_preloader,
            location: location.clone(),
            active_loops: vec![],
            last_step_id: StepId(-1),
            last_expr_order: vec![],
            diff_lines,
            diff_call_keys,
            mode,
            trace_kind,
            lang: lang_from_context(&Path::new(&location.path), trace_kind),
        }
    }

    // TODO:
    // refactor load_flow to several methods

    // TODO:
    //   we can add a NonEmptyVec or VecWithSizeAtLeast<1, ..>
    //   and with limiting its API we can make sure
    //   a `[0]` or `last()` method is safe, and remove unwraps
    //
    //   for now manually we saw that most collections which we
    //   use last/last_mut on seem to have at least 1 element
    //   by construction or because we push to them before using
    //   last
    //
    #[allow(clippy::unwrap_used)]
    pub fn load_flow(&mut self, location: Location, replay: &mut dyn Replay) -> FlowUpdate {
        // Update location on flow load
        if self.mode == FlowMode::Call {
            // let step_id = StepId(location.rr_ticks.0);
            // let call_key = self.db.steps[step_id].call_key;
            // let function_id = self.db.calls[call_key].function_id;
            // let function_first = self.db.functions[function_id].line;
            // info!("load {arg:?}");

            self.location = self
                .flow_preloader
                .expr_loader
                .find_function_location(&location, &Line(location.line));
        }

        // info!("location flow {:?}", self.location);

        match self.load_view_update(replay) {
            Ok(flow_view_update) => {
                let mut flow_update = FlowUpdate::new();
                flow_update.location = self.location.clone();
                flow_update.view_updates.push(flow_view_update);
                flow_update.status = FlowUpdateState {
                    kind: FlowUpdateStateKind::FlowFinished,
                    steps: 0,
                };
                flow_update
            }
            Err(e) => {
                error!("flow error: {e:?}");
                FlowUpdate::error(&format!("{:?}", e))
            }
        }
    }

    fn add_return_value(&mut self, mut flow_view_update: FlowViewUpdate, replay: &mut dyn Replay) -> FlowViewUpdate {
        // assumes that replay is stopped on the place where return value is available

        let return_string = "return".to_string();

        // The if condition ensures, that the Options on which .unwrap() is called
        // are never None, so it is safe to unwrap them.
        if !flow_view_update.steps.is_empty() {
            info!("  try to load return value");
            let return_value_record = replay
                .load_return_value(Some(RETURN_VALUE_RR_DEPTH_LIMIT), self.lang)
                .unwrap_or(ValueRecordWithType::Error {
                    msg: "<return value error>".to_string(),
                    typ: TypeRecord {
                        kind: TypeKind::Error,
                        lang_type: "<error>".to_string(),
                        specific_info: TypeSpecificInfo::None,
                    },
                });
            let return_value = to_ct_value(&return_value_record);
            info!("  return value: {:?}", return_value);

            #[allow(clippy::unwrap_used)]
            flow_view_update
                .steps
                .last_mut()
                .unwrap()
                .before_values
                .insert(return_string.clone(), return_value.clone());

            #[allow(clippy::unwrap_used)]
            flow_view_update
                .steps
                .last_mut()
                .unwrap()
                .expr_order
                .push(return_string.clone());

            #[allow(clippy::unwrap_used)]
            flow_view_update
                .steps
                .first_mut()
                .unwrap()
                .before_values
                .insert(return_string.clone(), return_value.clone());

            #[allow(clippy::unwrap_used)]
            flow_view_update
                .steps
                .first_mut()
                .unwrap()
                .expr_order
                .push(return_string);
        }
        flow_view_update
    }

    fn next_diff_flow_step(
        &self,
        _from_step_id: StepId,
        _including_from: bool,
        _replay: &mut dyn Replay,
    ) -> (StepId, bool, bool) {
        // TODO: maybe combination of replay.next, diff_call_keys check, different for cases?s
        //
        // if from_step_id.0 >= db.steps.len() as i64 {
        //     (from_step_id, false)
        // } else {
        //     // TODO: next diff step
        //     let mut next_step_id = if !including_from { from_step_id + 1 } else { from_step_id };
        //     loop {
        //         if next_step_id.0 >= db.steps.len() as i64 { // must be + 1! then we assume we should stay and report not progressing
        //             return (from_step_id, false);
        //         }
        //         let next_step = db.steps[next_step_id];
        //         info!("check {:?}", (PathBuf::from(db.paths[next_step.path_id].clone()), next_step.line.0));
        //         if self.diff_call_keys.contains(&next_step.call_key.0) {
        //             // &(PathBuf::from(db.paths[next_step.path_id].clone()), next_step.line.0)) {
        //             return (next_step_id, true);
        //         } else {
        //             next_step_id = next_step_id + 1;
        //             continue;
        //         }
        //     }
        // }
        todo!()
    }

    fn move_to_first_step(
        &self,
        from_step_id: StepId,
        replay: &mut dyn Replay,
    ) -> Result<(StepId, bool, bool), Box<dyn Error>> {
        let (mut step_id, mut progressing, mut move_error) = match self.mode {
            FlowMode::Call => (from_step_id, true, false),
            FlowMode::Diff => self.next_diff_flow_step(StepId(0), true, replay),
        };
        if self.trace_kind == TraceKind::DB {
            // for now assume it can't fail: `?` only because of more general api
            replay.jump_to(step_id)?;
        } else {
            if let Ok(location) = replay.jump_to_call(&self.location) {
                step_id = StepId(location.rr_ticks.0);
                progressing = true;
            } else {
                move_error = true;
            }
        }
        Ok((step_id, progressing, move_error))
    }

    // returns new step_id/rr ticks(?) and `progressing`(if false, the flow loop should stop)
    //   for RR: rr ticks might stay the same, but we still return progressing `true` unless we have an error for
    //      stepping/location
    fn move_to_next_step(&mut self, from_step_id: StepId, replay: &mut dyn Replay) -> (StepId, bool, bool) {
        info!("  move_to_next_step:");
        match self.mode {
            FlowMode::Call => {
                // let step_to_different_line = true; // for flow for now makes sense to try to always reach a new line
                if let Err(e) = replay.step(Action::Next, true) {
                    // this might not really be a problem: we just need to stop the flow after this:
                    warn!("    `next` error: {e:}");
                    return (from_step_id, false, true); // assume we will break the flow loop if `progressing` is false
                }

                let mut expr_loader = ExprLoader::new(CoreTrace::default());
                // for DbReplay actually those replay methods shouldn't fail;
                //   but this might be unreliable/change in the future
                //   and for RR they can also surely fail
                match replay.load_location(&mut expr_loader) {
                    Ok(location) => {
                        let new_step_id = StepId(location.rr_ticks.0);
                        let progressing = if self.trace_kind == TraceKind::DB {
                            new_step_id != from_step_id
                        } else {
                            // for now hard to detect; assume true
                            //   we tried `new_step_id != from_step_id;`, but this is incorrect:
                            //   often rr ticks can be the same for many different lines.. maybe detect signal or
                            //   hope that difference in call key/other aspects will be enough!
                            true
                        };
                        (new_step_id, progressing, false)
                    }
                    Err(e) => {
                        warn!("    `load_location` error: {e:}");
                        // assume we will break the flow loop if `progressing` is false
                        (from_step_id, false, true)
                    }
                }
            }
            FlowMode::Diff => self.next_diff_flow_step(from_step_id, false, replay),
        }
    }

    fn call_key_from(&self, location: &Location) -> Result<CallKey, Box<dyn Error>> {
        Ok(CallKey(location.key.parse::<i64>()?)) // for now still assume it's an integer
    }

    fn load_view_update(&mut self, replay: &mut dyn Replay) -> Result<FlowViewUpdate, Box<dyn Error>> {
        // let start_step_id = StepId(self.location.rr_ticks.0);
        // db.calls[call_key].step_id;
        // let mut path_buf = &PathBuf::from(&self.location.path);
        let mut iter_step_id = StepId(self.location.rr_ticks.0);

        // later location updated after move to first step!
        let mut flow_view_update = FlowViewUpdate::new(self.location.clone());

        let mut step_count = 0;
        let mut before_first_move = true;
        // let tracked_call_key_result = self.call_key_from(&self.location);
        let mut tracked_call_key = NO_KEY;
        // match tracked_call_key_result {
        //     Ok(call_key) => {
        //         tracked_call_key = call_key;
        //     }
        //     Err(e) => {
        //         error!("call key parse error: {e:?}");
        //         return Err(e);
        //     }
        // }

        info!("flow loop:");
        loop {
            // let (step_id, progressing) = if first {
            //     first = false;
            //     self.find_first_step(iter_step_id, replay)
            // } else {
            //     self.find_next_step(iter_step_id, replay)
            // };
            let (step_id, progressing, move_error) = if before_first_move {
                before_first_move = false;
                self.move_to_first_step(iter_step_id, replay)?
            } else {
                self.move_to_next_step(iter_step_id, replay)
            };

            if move_error {
                info!("move error: break flow");
                break;
            }

            iter_step_id = step_id;
            let mut expr_loader = ExprLoader::new(CoreTrace::default());
            let new_location = replay.load_location(&mut expr_loader)?;
            if step_count == 0 {
                self.location = new_location.clone();
                tracked_call_key = self.call_key_from(&new_location)?;
                flow_view_update.location = self.location.clone();
            }

            info!(
                "  location for step count {}: {}:{}",
                step_count, new_location.path, new_location.line
            );

            let new_call_key = match self.call_key_from(&new_location) {
                Ok(call_key) => call_key,
                Err(e) => {
                    error!("error when parsing call key: stopping flow preload: {e:?}");
                    break;
                }
            };

            if self.mode == FlowMode::Call && tracked_call_key != new_call_key || !progressing {
                replay.step(Action::StepIn, false)?; // hopefully go back to the end of our original function
                let return_location = replay.load_location(&mut expr_loader)?;
                let mut load_return_value = false;
                // maybe this can be improved with a limited loop/jump to return/exit of call in the future
                if let Ok(return_call_key) = self.call_key_from(&return_location) {
                    if return_call_key == tracked_call_key {
                        flow_view_update = self.add_return_value(flow_view_update, replay);
                        load_return_value = true;
                    }
                }
                if !load_return_value {
                    warn!("we can't load return value");
                }
                if tracked_call_key != new_call_key {
                    info!(
                        "  a different call key now: tracked is: {:?} new is: {:?}",
                        tracked_call_key, new_call_key
                    );
                }
                if !progressing {
                    info!("  not progressing in stepping anymore");
                }
                info!("  break flow!");
                break;
            }

            if step_count >= STEP_COUNT_LIMIT as i64 {
                info!("  break flow because of step count limit");
                break;
            }

            let events = self.load_step_flow_events(replay, step_id);
            // for now not sending last step id for line visit
            // but this flow step object *can* contain info about several actual steps
            // e.g. events from some of the next steps on the same line visit
            // one can analyze the step id of the next step, or we can add this info to the object
            let line = new_location.line;
            flow_view_update.steps.push(FlowStep::new(
                line,
                step_count,
                replay.current_step_id(),
                Iteration(0),
                LoopId(0),
                events,
            ));
            flow_view_update.relevant_step_count.push(line as usize);
            flow_view_update.add_step_count(line, step_count);
            info!("  process loops");
            let path_buf = &PathBuf::from(&new_location.path);
            flow_view_update = self.process_loops(
                flow_view_update.clone(),
                Position(new_location.line),
                replay.current_step_id(),
                path_buf,
                step_count,
            );
            flow_view_update = self.log_expressions(
                flow_view_update.clone(),
                Position(new_location.line),
                replay,
                step_id,
                &new_location,
            );
            step_count += 1;
        }
        let path_buf = &PathBuf::from(&self.location.path);
        // TODO: maybe not true for diff flow, we can have multiple files/paths there
        flow_view_update.comment_lines = self.flow_preloader.expr_loader.get_comment_positions(path_buf);
        flow_view_update.add_branches(
            0,
            self.flow_preloader
                .expr_loader
                .final_branch_load(path_buf, &flow_view_update.branches_taken[0][0].table),
        );
        Ok(flow_view_update)
    }

    #[allow(clippy::unwrap_used)]
    fn process_loops(
        &mut self,
        mut flow_view_update: FlowViewUpdate,
        line: Position,
        step_id: StepId,
        path_buf: &PathBuf,
        step_count: i64,
    ) -> FlowViewUpdate {
        if let Some(loop_shape) = self.flow_preloader.expr_loader.get_loop_shape(line, path_buf) {
            info!("  loop shape {:?}", loop_shape);
            if loop_shape.first.0 == line.0 && !self.active_loops.contains(&loop_shape.first) {
                flow_view_update.loops.push(Loop {
                    base: LoopId(loop_shape.loop_id.0),
                    base_iteration: Iteration(0),
                    internal: vec![],
                    first: loop_shape.first,
                    last: loop_shape.last,
                    registered_line: loop_shape.first,
                    iteration: Iteration(0),
                    step_counts: vec![StepCount(step_count)],
                    rr_ticks_for_iterations: vec![RRTicks(step_id.0)],
                });
                self.active_loops.push(loop_shape.first);
                flow_view_update
                    .loop_iteration_steps
                    .push(vec![LoopIterationSteps::default()]);
                flow_view_update.branches_taken.push(vec![BranchesTaken::default()]);
                info!("    add an active loop");
            } else if (flow_view_update.loops.last().unwrap().first.0) == line.0 {
                flow_view_update.loops.last_mut().unwrap().iteration.inc();
                flow_view_update
                    .loop_iteration_steps
                    .last_mut()
                    .unwrap()
                    .push(LoopIterationSteps::default());
                flow_view_update
                    .branches_taken
                    .last_mut()
                    .unwrap()
                    .push(BranchesTaken::default());
                flow_view_update
                    .loops
                    .last_mut()
                    .unwrap()
                    .rr_ticks_for_iterations
                    .push(RRTicks(step_id.0));
                info!("    add iteration");
            }
        }

        if flow_view_update.loops.last().unwrap().first.0 <= line.0
            && flow_view_update.loops.last().unwrap().last.0 >= line.0
        {
            flow_view_update.steps.last_mut().unwrap().iteration =
                Iteration(flow_view_update.loops.last().unwrap().iteration.0);
            flow_view_update.steps.last_mut().unwrap().r#loop = flow_view_update.loops.last().unwrap().base.clone();
            let index = (flow_view_update.loops.last().unwrap().base.0) as usize;
            if index < flow_view_update.loop_iteration_steps.len() {
                flow_view_update
                    .loops
                    .last_mut()
                    .unwrap()
                    .step_counts
                    .push(StepCount(step_count));
                flow_view_update.loop_iteration_steps[index]
                    .last_mut()
                    .unwrap()
                    .table
                    .insert(line.0 as usize, step_count as usize);
                flow_view_update.add_branches(
                    flow_view_update.loops.clone().last_mut().unwrap().base.0,
                    self.flow_preloader.expr_loader.load_branch_for_position(line, path_buf),
                );
                info!("    add branch for position {:?} {:?}", path_buf.display(), line);
            }
        } else {
            flow_view_update.loop_iteration_steps[0][0]
                .table
                .insert(line.0 as usize, step_count as usize);
            flow_view_update.add_branches(
                0,
                self.flow_preloader.expr_loader.load_branch_for_position(line, path_buf),
            );
            info!("    add branch for position {:?} {:?}", path_buf.display(), line);
        }
        info!("    branches taken {:?}", flow_view_update.branches_taken);
        flow_view_update
    }

    fn to_flow_event(&self, event: &DbRecordEvent) -> FlowEvent {
        FlowEvent {
            kind: event.kind,
            text: event.content.clone(),
            rr_ticks: event.step_id.0,
            metadata: event.metadata.clone(),
        }
    }

    fn load_step_flow_events(&self, replay: &mut dyn Replay, step_id: StepId) -> Vec<FlowEvent> {
        // load not only exactly this step, but for the whole step line "visit":
        // include events for next steps for this visit, because we don't process those steps in flow
        // otherwise, but we do something like a `next`
        let exact = false;
        let step_events = replay.load_step_events(step_id, exact);
        let flow_events = step_events.iter().map(|event| self.to_flow_event(event)).collect();
        // info!("flow events: {flow_events:?}");
        #[allow(clippy::let_and_return)] // useful to have the variable for debugging/logging
        flow_events
    }

    #[allow(clippy::unwrap_used)]
    fn log_expressions(
        &mut self,
        mut flow_view_update: FlowViewUpdate,
        line: Position,
        replay: &mut dyn Replay,
        step_id: StepId,
        location: &Location,
    ) -> FlowViewUpdate {
        let mut expr_order: Vec<String> = vec![];
        let mut variable_map: HashMap<String, Value> = HashMap::default();

        // for value_record in &db.variables[step_id] {
        //     variable_map.insert(
        //         db.variable_names[value_record.variable_id].clone(),
        //         value_record.clone(),
        //     );
        // }

        // for (variable_id, place) in &db.variable_cells[step_id] {
        //     let value_record = db.load_value_for_place(*place, step_id);
        //     let full_value_record = FullValueRecord {
        //         variable_id: *variable_id,
        //         value: value_record,
        //     };
        //     let name = db.variable_name(*variable_id);
        //     variable_map.insert(name.clone(), full_value_record);
        // }

        if let Some(var_list) = self.flow_preloader.get_var_list(line, location) {
            info!("  log expressions: {:?}", var_list.clone());
            for value_name in &var_list {
                if let Ok(value) = replay.load_value(value_name, Some(LOAD_FLOW_VALUE_RR_DEPTH_LIMIT), self.lang) {
                    // if variable_map.contains_key(value_name) {
                    let ct_value = to_ct_value(&value);
                    flow_view_update
                        .steps
                        .last_mut()
                        .unwrap()
                        .before_values
                        .insert(value_name.clone(), ct_value.clone());
                    info!("    insert in variables_map {}", value_name);
                    variable_map.insert(value_name.clone(), ct_value);
                }
                expr_order.push(value_name.clone());
            }

            flow_view_update.steps.last_mut().unwrap().expr_order = expr_order.clone();
        }

        if self.last_step_id.0 >= 0 && flow_view_update.steps.len() >= 2 {
            let index = flow_view_update.steps.len() - 2;

            for variable in &self.last_expr_order {
                if variable_map.contains_key(variable) {
                    flow_view_update.steps[index]
                        .after_values
                        .insert(variable.clone(), variable_map[variable].clone());
                }
            }
        }

        self.last_step_id = step_id;
        self.last_expr_order = expr_order;
        flow_view_update
    }
}
